przystawka mmc (odpalana za pomocą aplikacji run wpisując mmc) pozwala na sprawdzenie wersji sql server oraz baz danych na maszynie
w sql n oznacza znak w UTF8 (np typ nvarchar lub N'ąćęł')
w sql sposób sortowanie musi być ustalony w trakcie tworzenia kolumny (później nie da się go zmienić)
skrypty do migracji bazy danych powinny być idioto-odporne (np. sprawdzanie czy kolumna istnieje przed jej dodaniem), dobrym sposobem jest wersja tabeli w bazie danych (np. w osobnej tabeli)
public class Foo{
public static int Bar;
public static int BarMethod(){
int t = 0;
return 42;
}
public int Qux;
public int QuxMethod(){
int t = 0;
return 42;
}
}
czym różni się pole statyczne od metody statycznej?
// wywołanie
Foo.BarMethod();
(new Foo()).QuxMethod();
a co się dzieje z polem?
var _ = Foo.Bar; // współdzielone
var __ = (new Foo()).Qux; // ulotna, bezpieczniejsza
zmienne lokalne w metodzie współdzielonej nie są współdzielone
bezpieczeństwo wywołania metody we współdzielonych wątkach sprawdzamy w dokumentacji w sekcji 'thread safety'
mamy 3 rodzaje kontenerów zarządzających czasem życia obiektów:
this.Application static; interfejs .Add() oraz indexer (this.Application["item name"])this.Session jeden użytkownik na wszystkie żądania; interfejs analogiczne do Applicationthis.Items od początku żądania do końca żądania(kontenery te są niegeneryczne czyli zwracają Object i trzeba używać rzutowania)
obiekt wspólny dla wszystkich endpointów to
HttpContext context, wtedy mamy dostęp do npcontext.Items
Jak dbać o dobry podział solution na projekty?
po pierwsze dbamy o to aby mieć odpowiedni fizyczny układ folderów
po drugie na poziomie solution możemy tworzyć wirtualne foldery na projekty
w projekcie aplikacji webowej byłyby wszystkie pliki odpowiedzialne za front-end
dodatkowe moduły np odpowiedzialne za jakąś logikę byłyby w osobnym projekcie
oddzielmy dostęp do bazy danych od aplikacji webowej
public class CustomSqlConnectionSource {
const string CONTEXTKEYNAME = "sqlconnectionkey";
public SqlConnection GetConnection(HttpContext context){
if(context.Items[CONTEXTKEYNAME] == null){
var cs2 = ConfigurationManager.AppSettings["cs2"];
var cs1 = ConfigurationManager.ConnectionStrings["cs1"].ConnectionString;
var conn = new SqlConnection(cs1);
context.Items.Add(CONTEXTKEYNAME, conn);
}
return (SqlConnection)context.Items[CONTEXTKEYNAME];
}
}
użytkownik główny aplikacji powinien dostać prawa właściciela (owner) bazy danych tej aplikacji
w Web.config dodajemy connection string
<!--! 2 metody -->
<configuration>
<appSettings>
<add key="cs2" value="server=.\sqlexpress;database=example;integrated security=true;trustservercertificate=true"/>
</appSettings>
<connectionStrings>
<add name="cs1" connectionString="server=.\sqlexpress;database=example;integrated security=true;trustservercertificate=true"/>
</connectionStrings>
</configuration>
teraz w naszej aplikacji webowej możemy korzystać z CustomSqlConnectionSource
public void Page_Load(object sender, EventArgs e){
var conn = new CustomSqlConnectionSource().GetConnection(HttpContext);
var persons = conn.Query<Person>("select * from dbo.Person");
var personList = string.Join(", ", persons.Select(p => p.Name));
}
mamy taki zasób jak GlobalApplicationClass, który ma funkcję Application_End lub Application_EndRequest
możemy też skorzystać z zasobu AbstractBasePage którą wepchniemy wyżej w hierarchii naszych klas
do naszej strony dodajemy interfejs IDisposable, który na koniec życia obiektu <=> trwania requesta wywoła się, i będziemy mogli wyrzucić obiekt
w .NET Core
var builder = WebApplication.CreateBuilder(args);
builder.Services
// scoped <=> per żądanie
.AddScoped<Foo>(services =>
{
return new Foo();
})
// lub
.AddSingleton
// lub
.AddTransient
app.MapGet("/", (Foo foo) => "Hello World");
public class Foo {
}
nauczyliśmy teraz kontenera jak ma tworzyć obiekty Foo, jak z tego skorzystać do łączenia z bazą danych?
bulider.Services
.AddScoped<SqlConnection>(service =>
{
IConfiguration cfg = service.GetRequiredService<IConfiguration>();
var cs1 = cfg["cs1"];
return new SqlConnection(cs1);
});
app.MapGet("/", (SqlConnection conn) => {
// tutaj potrzebny jest dapper
var persons = conn.Query<Person>("select * from dbo.Person");
return string.Join(", ", persons.Select(p => p.Name));
});
connection string możemy dodać w appsettings.json
{
"cs1": "server=.\sqlexpress;database=example;integrated security=true;trustservercertificate=true"
}
tutaj już nie musimy dodawać manualnie Dispose, bo kontener sam zadba o to przy końcu requesta
widoki muszą być w folderze Views, natomiast modele i kontrolery mogą być w dowolnym miejscu (aczkolwiek wg konwencji w odpowiednich folderach)
Typt autentykacji:
można podmienić w tym statusie sposób autentykacji, np na opartą o protokół Cerberos - w samym Windowsie wbudowane są różne funkcje (typu daj token użytkownika, daj nazwę tego użytkownika)
Wtedy w samym C# będziemy mieli Request.User z metodami dla już zalogowanego użytkownika. pytanie jak się ten użytkownik uwierzytelnia?
dlaczego token JWT (doklejony do nagłówka zapytania) jest lepszy od ciastka?
bo ciastka nie są cross-domenowe, jeżeli zapytania będą wysyłane na różne serwery, to token z automatu będzie przenoszony do dowolnego serwera (np.: nasza jedna strona ma jakieś podaplikacje, chcemy aby użytkownik był uwierzytelniony we wszystkich, nie musząc się logować dla każdej z osobna)
this.User przychodzi standarwo wraz z .NET
PageLoad {
this.Label1.Text = $"username: {this.User.Identity.Name}, isauthenticated: {this.User.Identity.IsAuthenticated}";
}
Aby upewnić się, że użytkownik jest zalogowany, w MVC będziemy pisali atrybut. W webformsaach musimy w web.config dodać:
<system.web>
<authetincation mode="Forms">
<forms name="foo" loginUrl="/Login.aspx" />
</authentication>
<authorization>
<deny users="?" /> <!-- odmawiaj niezalogowanym -->
<allow users="*" /> <!-- zezwalaj zalogownym -->
</authorization>
</system.web>
możliwe tryby: Forms - 302
loginUrl to strona logowania, na którą będzie przekierowany użytkownik
jeżeli chcemy inne reguły np dla admina to tworzymy dla niego osobny folder, np ForAdmin, i tam tworzymy kolejny web.config gdzie ustawiamy dla niego zsady autentykacji
co wykonuje przekierowania z web.config? jakiś wbudowany moduł odpowiedzialny za autentykację
teraz możemy napisać stronę logowania, skorzystamy do niej z modułu forms authentication:
PaleLoad() {
}
Button_Click() {
if(hasło jest dobre) {
var ticket = new FormAuthenticationTicket(TextBoxName.Text, false, 20); // drugie pole to pytanie o utrwalenie ciastka - zapisanie go na jakiś czas (nie jest to to samo co ważność ciastka, jest to po stronie serwera)
var cookieValue = FormsAuthentication.Encrypt(ticket);
var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, cookieValue);
this.Response.AppendCookie(cookie);
var redirectUrl = this.Request.QueyString["returnUrl"];
this.Response.Redirect(redirectUrl);
}
}
możemy dodać opcję slidingExpiration, która gdy minie połowa czasu ważności ciastka, przedłuża jego ważność (również po stronie serwera), wtedy tak długo jak użytkownik jest aktywny pozostanie on zalogowany, a po zamknięciu przeglądarki zostanie wylogowany
<authentication mode>
<forms name="foo" loginurl="" slidingExpiration="true" />
</authentication>
Skąd wiedzieć kiedy pokazać użytkownikowi CAPTCHA? (bo chcemy tylko takiemu, który ma jakieś nieudane próby logowania)
robimy 2-krokowe logowanie - najpierw pokazujemy tylko login, potem pytamy serwer o ten login i on nam odpowiada czy wyświetlić mu CAPTCHA przy wpisywaniu hasła
w notatkach powinna być
żeby wymusić autentykację używamy atrybutu [Authorize] lub [Autrize(schemat)]
w Main dodajemy do buildera autentykację (z notatek) (tutaj możemy tez np dodać własny cookie builder, albo cookie manager)
w app dodajemy middleware (z notatek)
reszta jest analogicznie jak wcześniej
https://github.com/wzychla/Fido2.NetFramework
REST jest bardziej dedykowany dla komunikacji serwer->przeglądarka, natomiast SOAP jest bardziej dedykowany dla komunikacji serwer->serwer
tworząc nowy projekt wybieramy WebAPI oraz MVC (możemy skorzystac z obu)
w folderze controllers zrobimy osobne foldery na MVC oraz WebAPI, żeby nam się nie pomieszały
./App_Start/RouterConfig.cs - zawiera konfigurację routigu dla MVC
./App_Start/WebApiConfig.cs - zawiera konfigurację routingu dla WebAPI
dlatego że MVC ma być ścieżką domyślną to zarejestrujemy ją po WebAPI
żeby ścieżki idące do kontrolerów WebAPI były rozróżniane od tych idących do kontrolerów MVC, dodajemy prefix api do ścieżki: api/{controller}/{id}
dlaczego w WebAPI nie ma w ścieżce {action}?
bo w WebAPI mamy tylko jedną akcję, dla GET/POST/PUT/DELETE
tworząc kontroler do WebAPI musimy wybrać (w trakcie tworzenia) odpowiedni szablon! (w nim klasa kontrolera dziedziczy po ApiController)
z nazwy funkcji kontrolera w weapi wynika jaką akcję obsługuje
public class PersonController : ApiController
{
// oba zapytania działają, bo są rozróżnione po ścieżce
public Person Get()
{
return new Person { Name = "Jan" };
}
public Person Get(string id)
{
return new Person { Name = "Jan" + id };
}
}
public class Person
{
public string Name { get; set; }
}
domyślnie zostanie zwrócony XML, aby to zmienić dodajemy w WebApiCongig.cs:
config.Formatters.Remove(GlobalConfiguration.Configuration.Formatters.XmlFormatter);
zasada na oko: do 3 parametrów w URL zapytania GET mają jeszcze sens
public PersonPostResponseModel Post(PersonPostRequestModel model)
{
return new PersonPostResponseModel();
}
public class PersonPostRequestModel {}
public class PersonPostResponseModel {}
public IHttpActionResult Get(string id)
{
// tutaj np możemy przeproawdzić weryfikację zapytania
if(id == "foo") {
return this.BadRequest();
}
return this.Ok(
new Person({ Name = "Jan" });
);
}
Tworzymy po prostu nowy pusty projekt (ewentualnie skorzystamy z szablonu dla MVC - jest już tam skonfigurowany routing)
W core kontrolery dla webapi i dla mvc to te same routy
tworzymy z dedykowanego szblonu dla webapi - dziedziczy on po ControllerBase oraz ma nadpisaną ścieżkę [Route("api"/[controller])]
działa to praktycznie tak samo jak w .NET Framework
public IActionResult Get()
{
return this.Ok(new Person(){} );
}
w .NET Core jak ustawimy funkcji w kontrolerze atrybut [HttpGet] to nasza funkcja może mieć już dowolną nazwę
przy komunikacji serwer-serwer nie mamy dostępu do ciasteczek = autentykacji, więc potrzebujemy innego sposobu na autentykację - kluczy z dostatecznie dużą entropią
do tego możemy stworzyć własny filtr do autentykacji za pomocą api key, wtedy zamiast [Authorize] nad funkcją będziemy pisać [CustomAuthenticationFilter]
w standardzie mamy już dedykowany nagłówek Authorization który ma albo wartość Basic która trzymma proste klucze (np klucz api), albo coś bardziej zkomplikowanego
Sensowna alternatywa dla prymitywnych uwierzytelnień stałym kluczem (mogą się często zmieniać, przez co wykradnięcie ich nie tworzy dużego ryzyka)
niestety dla nich w .NET Framework musimy sami napisać filtry autentykacyjne
w .NET Core do autentykacji służy funkcja .JWTAddBearer, która pozwala nam zweryfikować token jwt
wtedy w nagłówku HTML mamy "Authorization": Bearer ${token}, gdzie bearer jest dedykowany dla 'wygasających' kluczy
walidacja symetryczna vs asymetryczna:
to plik opisujący nasz serwis (pratkycznie zawsze jest on generowany automatycznie, nikt tego nie robi manualnie)
można go wygenerować jakimś narzędziem, i na podstawie takiego pliku stworzyć serwer oraz klienta, albo można też stworzyć serwer, z niego wyprodukować plik WSDL, a na podstawie tego pliku stworzyć klienta (co na tym wykładzie przećwiczymy)
tworzymy kompletnie pusty asp.net framework web app
aktualnie w asp.net core też można już za pomocą WCF konstruować serwisy SOAP
dodajemy web service ASMX
tutaj nie mamy części klienckiej, tylko jest zapytanie o wynik oraz jego zwrócenie - to już będzie nasza usługa sieciowa
po uruchomieniu serwera i przejściu na link, (paradoksalnie) pojawia się jakaś strona - jest to strona z opisem naszej usługi sieciowej (zawiera ona link do pliku WSDL przechodząc z parametrem zapytania ?wsdl)
przykład:
tworzymy modele
public class WebService1RequestModel
{
public string Name { get; set; }
public int Age { get; set; }
}
public class WebService1ResponseModel
{
public string Name { get; set; }
public int Age { get; set; }
}
następnie dodajemy metodę do naszego serwisu
[WebMethod]
public WebService1ResponseModel WebService1(WebService1RequestModel model)
{
return new WebService1ResponseModel
{
Name = model.Name,
Age = model.Age
};
}
stwórzmy nowy projekt webforms (dzięki temu że jest on w tym samym solution to będzie się on z naszym serwerem po localhost komunikować)
znajdujemy wsdl.exe w sdk dla .NET Framework, i generujemy na jego podstawie plik WebService1.cs
wsdl.exe http://localhost:1234/WebService1.asmx?wsdl
ewentualnie dzięki visual studio możemy zrobić to samo, klikając prawym na projekt i wybierając Add Service Reference; wtedy możemy wybrać naszą usługę i stworzyć serwis sieciowy
utworzy się wtedy klasa proxy, która pozwala nam na komunikację z serwerem
podepnijmy teraz pod przycisk w webforms naszą akcję
protected void Button1_Click(object sender, EventArgs e)
{
WebService1SoapClient client = new WebService1SoapClient();
var response = client.WebService1(new WebService1RequestModel
{
Name = "Jan",
Age = 42
});
MessageBox.Show(response.Name + " " + response.Age);
}
tworzenie analogiczne, ale wybieramy WCF Service
w kodzie mamy IService1.cs oraz Service1.svc
zaimplementujmy poprzedni przykład:
IService1.cs
[ServiceContract]
public interface IService1
{
[OperationContract]
WebService1ResponseModel WebService1(WebService1RequestModel model);
}
Service1.svc.cs
public class Service1 : IService1
{
public WebService1ResponseModel WebService1(WebService1RequestModel model)
{
return new WebService1ResponseModel
{
Name = model.Name,
Age = model.Age
};
}
}
teraz możemy dodać klienta, ale został on przyspieszony, żeby z tego skorzystać musimy kliknąć w advanced, gdzie odpali się nowe okno do dodawania klientów nowego typu
przykład z wykładu - jak to akutlanie się robi
hands on z wykładu - gdzie zaimplementować logikę obsługi maili?
tworzymy nowy projekt PortsImpl w którym tworzymy klasę EmailSender dziedziczącą co IEmailSender z metodą SendEmail
teraz możemy w starcie naszego programu (w Program.cs) zarejestrować nasz serwis w kontenerze DI
bulider.Services.AddScoped<IEmailSender, EmailSender>();
takie miejsce, na konfigurację aplikacji naszymi bibliotekami, jest nazywane CompositionRoot i możemy je wyekstraktować do osobnej klasy CompositionRoot
private static void CompositionRoot(WebApplicationBuilder builder)
{
builder.Services.AddScoped<IEmailSender, EmailSender>();
}
teraz możemy łatwo napisać testy jednostkowe
skorzystamy z biblioteki mock do wytwarania typów zastępczych (w runtime generuje typów które implementują dany interfejs)
namespace UnitTests.LogonUseCaseTestsSpace
{
[TestClass]
public class LogonUseCaseTests
{
[TestMethod]
public async Task SucessScenario()
{
var emailPortMock = new Mock<IEmailSender>();
var useUseCase = new LogonUseCase(emailPortMock.Object);
var result = await useUseCase.Handle(new LogonUseCaseRequestModel
{
Password = "foo",
Username = "bar"
}, new CancellationToken());
Assert.IsTrue(result.Success);
Assert.AreEqual("bar", result.Username);
emailPortMock.Verify(x => x.SendEmail(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
}
}
}